#!/usr/bin/env luajit
-- -*- lua -*-
--
-- Copyright (C) 2014-2020 Markus Klotzbuecher <mk@mkio.de>
--
-- SPDX-License-Identifier: BSD-3-Clause
--

local utils = require "utils"
local umf = require "umf"
local ts = tostring
local fmt = string.format

---
--- ubx block meta-model
---
local NumberSpec=umf.NumberSpec
local BoolSpec=umf.BoolSpec
local StringSpec=umf.StringSpec
local TableSpec=umf.TableSpec
local ObjectSpec=umf.ObjectSpec
local EnumSpec=umf.EnumSpec

block = umf.class("block")

-- types
local types_spec = TableSpec{
   name="types",
   array = {
      TableSpec {
	 name="type",
	 dict={ name=StringSpec{}, class=EnumSpec{ "struct" }, doc=StringSpec{} },
	 sealed='both',
	 optional={ 'doc' },
      },
   },
   sealed='both'
}

-- configurations
local configurations_spec = TableSpec{
   name="configurations",
   array = {
      TableSpec {
	 name="configuration",
	 dict={ name=StringSpec{}, type_name=StringSpec{}, min=NumberSpec{}, max=NumberSpec{}, doc=StringSpec{} },
	 sealed='both',
	 optional={ 'len', 'doc', 'min', 'max' },
      },
   },
   sealed='both'
}

-- configurations
local ports_spec = TableSpec{
   name="ports",
   array = {
      TableSpec {
	 name="port",
	 dict={
	    name=StringSpec{},
	    in_type_name=StringSpec{},
	    in_data_len=NumberSpec{ min=1 },
	    out_type_name=StringSpec{},
	    out_data_len=NumberSpec{ min=1 },
	    doc=StringSpec{}
	 },
	 sealed='both',
	 optional={ 'in_type_name', 'in_data_len', 'out_type_name', 'out_data_len', 'doc' }
      },
   },
   sealed='both'
}

local block_spec = ObjectSpec {
   name="block",
   type=block,
   sealed="both",
   dict={
      name=StringSpec{},
      meta_data=StringSpec{},
      license=StringSpec{},
      configure_ac=BoolSpec{},
      port_cache=BoolSpec{},
      types=types_spec,
      configurations=configurations_spec,
      ports=ports_spec,
      operations=TableSpec{
	 name='operations',
	 dict={
	    start=BoolSpec{},
	    stop=BoolSpec{},
	    step=BoolSpec{},
	 },
	 sealed='both',
	 optional={ "start", "stop", "step" },
      },
   },
   optional={ 'meta_data', 'types', 'configurations', 'ports', 'configure_ac' },
}

--- Validate a block model.
function block:validate(verbose)
   return umf.check(self, block_spec, verbose)
end

local function usage()
   print( [[
ubx-genblock: generate the code for an empty microblx block.

Usage: genblock [OPTIONS]
   -c           block specification file
   -check       only check block specification, don't generate
   -force       force regeneration of all files and the type header files
   -cpp		force generation of a C++ block
   -d		output directory (will be created)
   -h           show this.
]])
end

---
--- Code generation functions and templates
---

--- Generate a struct type stub
-- @param data
-- @param fd file to write to (optional, default: io.stdout)
local function generate_struct_type(fd, typ)
   fd = fd or io.stdout
   local guardsym = string.upper(typ.name)
   local res, str = utils.preproc(
[[
/* generated type stub, extend this struct with real information */
#ifndef $(guardsym)_H
#define $(guardsym)_H

struct $(type_name) {
	/* TODO: fill in body */
};

#endif /* $(guardsym)_H */
]], { table=table, type_name=typ.name, guardsym=guardsym } )

   if not res then error(str) end
   fd:write(str)
end

--- Generate type read/write helpers
-- @param block_model
-- @return string
local function generate_type_accessors(bm)
   local res = {}

   for _,p in ipairs(bm.types or {}) do
	 res[#res+1] = utils.expand("def_type_accessors($name, struct $name);", { name=p.name })
   end

   return table.concat(res, "\n")
end

--- Generate configure.ac
-- @param bm block model
-- @param fd file to write to (optional, default: io.stdout)
local function generate_bootstrap(fd, bm)
fd:write([[
#!/bin/sh

autoreconf --install || exit 1
]])
end


--- Generate configure.ac
-- @param bm block model
-- @param fd file to write to (optional, default: io.stdout)
function generate_configure_ac(fd, bm)

   local res, str = utils.preproc([[
m4_define([package_version_major],[0])
m4_define([package_version_minor],[0])
m4_define([package_version_micro],[1])

AC_INIT([$(bm.name)], [package_version_major.package_version_minor.package_version_micro])
AM_INIT_AUTOMAKE([foreign -Wall])

# compilers
@ if bm.cpp then
AC_PROG_CXX
@ else
AC_PROG_CC
@ end

PKG_PROG_PKG_CONFIG
PKG_INSTALLDIR

AC_CONFIG_HEADERS([config.h])
AC_CONFIG_MACRO_DIR([m4])

# Check if the `install` program is present
AC_PROG_INSTALL

m4_ifdef([AM_PROG_AR], [AM_PROG_AR])
LT_INIT(disable-static)

PKG_CHECK_MODULES(UBX, ubx0 >= 0.9.0)

PKG_CHECK_VAR([UBX_MODDIR], [ubx0], [UBX_MODDIR])
  AC_MSG_CHECKING([ubx module directory])
  AS_IF([test "x$UBX_MODDIR" = "x"], [
  AC_MSG_FAILURE([Unable to identify ubx module path.])
])
AC_MSG_RESULT([$UBX_MODDIR])

AC_CONFIG_FILES([Makefile])
AC_OUTPUT

]], { bm=bm, table=table} )

   if not res then error(str) end
   fd:write(str)
end


--- Generate Makefile.am
-- @param bm block model
-- @param fd file to write to (optional, default: io.stdout)
function generate_automakefile(fd, bm)

   -- Generate the BUILT_SOURCES directive
   local function gen_built_sources()
	 if not bm.types or #bm.types < 1 then return "" end
	 local tab = { "BUILT_SOURCES = types/" .. bm.types[1].name..".h.hexarr" }
	 for i=2,#bm.types do
	    tab[#tab+1] = "                types/" .. bm.types[i].name..".h.hexarr"
	 end
	 return table.concat(tab, "\\\n")
   end

   fd = fd or io.stdout
   local res, str = utils.preproc(
      [[

ubxmoddir = ${UBX_MODDIR}
ubxmod_LTLIBRARIES = $(bm.name).la

$(gen_built_sources())

CLEANFILES = $(BUILT_SOURCES)

@ if bm.cpp then
$(bm.name)_la_SOURCES = $(bm.name).cpp
@ else
$(bm.name)_la_SOURCES = $(bm.name).c
@ end
$(bm.name)_la_LDFLAGS = -module -avoid-version -shared -export-dynamic @UBX_LIBS@
@ if bm.cpp then
$(bm.name)_la_CPPFLAGS = -I${top_srcdir}/libubx @UBX_CFLAGS@ -fvisibility=hidden -Wno-missing-field-initializers
@ else
$(bm.name)_la_CFLAGS = -I${top_srcdir}/libubx @UBX_CFLAGS@ -fvisibility=hidden
@ end
%.h.hexarr: %.h
	ubx-tocarr -s $< -d $<.hexarr

]], { bm=bm, gen_built_sources=gen_built_sources, table=table })

   if not res then error(str) end
   fd:write(str)
end

--- Generate an entry in a port declaration.
-- Moved out due to improve readability of the conditional
-- @param t type entry
-- @return designated C initializer string
local function gen_port_decl(t)
   t.in_data_len = t.in_data_len or 1
   t.out_data_len = t.out_data_len or 1
   t.doc = t.doc or ''

   if t.in_type_name and t.out_type_name then
      return utils.expand('{ .name="$name", .out_type_name="$out_type_name", .out_data_len=$out_data_len, .in_type_name="$in_type_name", .in_data_len=$in_data_len, .doc="$doc" },', t)
   elseif t.in_type_name then
      return utils.expand('{ .name="$name", .in_type_name="$in_type_name", .in_data_len=$in_data_len, .doc="$doc"  },', t)
   elseif t.out_type_name then
      return utils.expand('{ .name="$name", .out_type_name="$out_type_name", .out_data_len=$out_data_len, .doc="$doc"  },', t)
   end
end

local function doublequote(v)
   if v ~= nil then return fmt('"%s"', v) end
end

--- Generate the interface of an ubx block.
-- @param bm block model
-- @param fd open file to write to (optional, default: io.stdout)
local function generate_block_if(fd, bm)
   fd = fd or io.stdout
   local res, str = utils.preproc(
[[
/*
 * $(bm.name) microblx function block (autogenerated, don't edit)
 */

#include <ubx/ubx.h>

UBX_MODULE_LICENSE_SPDX($(bm.license))

/* includes types and type metadata */
@ for _,t in ipairs(bm.types or {}) do
#include "types/$(t.name).h"
#include "types/$(t.name).h.hexarr"
@ end

ubx_type_t types[] = {
@ for _,t in ipairs(bm.types or {}) do
	def_struct_type(struct $(t.name), &$(t.name)_h),
@ end
};

/* block meta information */
char $(bm.name)_meta[] =
	" { doc='',"
	"   realtime=true,"
	"}";

/* declaration of block configuration */
ubx_proto_config_t $(bm.name)_config[] = {
@ for _,c in ipairs(bm.configurations or {}) do
@       c.doc = c.doc or ''
@       c.min = c.min or 0
@       c.max = c.max or 0
	{ .name="$(c.name)", .type_name = "$(c.type_name)", .min=$(c.min), .max=$(c.max), .doc="$(c.doc)" },
@ end
	{ 0 },
};

/* declaration port block ports */
ubx_proto_port_t $(bm.name)_ports[] = {
@ for _,p in ipairs(bm.ports or {}) do
	$(gen_port_decl(p))
@ end
	{ 0 },
};

@ if bm.port_cache then
/* declare a struct port_cache */
struct $(bm.name)_port_cache {
@    for _,t in ipairs(bm.ports or {}) do
	ubx_port_t* $(t.name);
@    end
};

/* helper function to cache ports. call in init */
static void update_port_cache(ubx_block_t *b, struct $(bm.name)_port_cache *pc)
{
@    for _,t in ipairs(bm.ports or {}) do
	pc->$(t.name) = ubx_port_get(b, "$(t.name)");
@    end
}
@ end -- if bm.port_cache

/* define safe accessors for the new types */
$(generate_type_accessors(bm))

/* block operation forward declarations */
int $(bm.name)_init(ubx_block_t *b);
@ if bm.operations.start then
int $(bm.name)_start(ubx_block_t *b);
@ end
@ if bm.operations.stop then
void $(bm.name)_stop(ubx_block_t *b);
@ end
void $(bm.name)_cleanup(ubx_block_t *b);
@ if bm.operations.step then
void $(bm.name)_step(ubx_block_t *b);
@ end

ubx_proto_block_t $(bm.name)_block = {
	.name = "$(bm.name)",
	.meta_data = $(bm.name)_meta,
	.type = BLOCK_TYPE_COMPUTATION,
	.configs = $(bm.name)_config,
	.ports = $(bm.name)_ports,

	/* ops */
	.init = $(bm.name)_init,
@    if bm.operations.start then
	.start = $(bm.name)_start,
@    end
@    if bm.operations.stop then
	.stop = $(bm.name)_stop,
@    end
	.cleanup = $(bm.name)_cleanup,
@    if bm.operations.step then
	.step = $(bm.name)_step,
@    end
};


/* $(bm.name) module init and cleanup functions */
int $(bm.name)_mod_init(ubx_node_t* nd)
{
	ubx_log(UBX_LOGLEVEL_DEBUG, nd, "%s", __func__);

	for (unsigned int i=0; i<ARRAY_SIZE(types); i++) {
		if(ubx_type_register(nd, &types[i]) != 0)
			return -1;
	}

	if(ubx_block_register(nd, &$(bm.name)_block) != 0)
		return -1;

	return 0;
}

void $(bm.name)_mod_cleanup(ubx_node_t *nd)
{
	ubx_log(UBX_LOGLEVEL_DEBUG, nd, "%s", __func__);

	for (unsigned int i=0; i<ARRAY_SIZE(types); i++)
		ubx_type_unregister(nd, types[i].name);

	ubx_block_unregister(nd, "$(bm.name)");
}

/* declare module init and cleanup functions, so that the ubx core can
 * find these when the module is loaded/unloaded */
UBX_MODULE_INIT($(bm.name)_mod_init)
UBX_MODULE_CLEANUP($(bm.name)_mod_cleanup)
]], { gen_port_decl = gen_port_decl,
      doublequote = doublequote,
      ipairs = ipairs, table = table,
      bm = bm, generate_type_accessors = generate_type_accessors } )

   if not res then error(str) end
   fd:write(str)
end


--- Generate the interface of an ubx block.
-- @param bm block model
-- @param fd open file to write to (optional, default: io.stdout)
local function generate_block_body(fd, bm)
   fd = fd or io.stdout
   local res, str = utils.preproc(
[[

@ if bm.cpp then
#include "$(bm.name).hpp"
@ else
#include "$(bm.name).h"
@ end

/* define a structure for holding the block local state. By assigning an
 * instance of this struct to the block private_data pointer (see init), this
 * information becomes accessible within the hook functions.
 */
struct $(bm.name)_info
{
	/* add custom block local data here */

@ if bm.port_cache then
	/* this is to have fast access to ports for reading and writing, without
	 * needing a hash table lookup */
	struct $(bm.name)_port_cache ports;
@ end
};

/* init */
int $(bm.name)_init(ubx_block_t *b)
{
	int ret = -1;
	struct $(bm.name)_info *inf;

	/* allocate memory for the block local state */
@ if bm.cpp then
	if ((inf = (struct $(bm.name)_info*) calloc(1, sizeof(struct $(bm.name)_info)))==NULL) {
@ else
	if ((inf = calloc(1, sizeof(struct $(bm.name)_info)))==NULL) {
@ end
		ubx_err(b, "$(bm.name): failed to alloc memory");
		ret=EOUTOFMEM;
		goto out;
	}
	b->private_data=inf;
@ if bm.port_cache then
	update_port_cache(b, &inf->ports);
@ end
	ret=0;
out:
	return ret;
}

@ if bm.operations.start then
/* start */
int $(bm.name)_start(ubx_block_t *b)
{
	/* struct $(bm.name)_info *inf = (struct $(bm.name)_info*) b->private_data; */
        ubx_info(b, "%s", __func__);
	int ret = 0;
	return ret;
}
@ end

@ if bm.operations.stop then
/* stop */
void $(bm.name)_stop(ubx_block_t *b)
{
	/* struct $(bm.name)_info *inf = (struct $(bm.name)_info*) b->private_data; */
        ubx_info(b, "%s", __func__);
}
@ end

/* cleanup */
void $(bm.name)_cleanup(ubx_block_t *b)
{
	/* struct $(bm.name)_info *inf = (struct $(bm.name)_info*) b->private_data; */
        ubx_info(b, "%s", __func__);
	free(b->private_data);
}

@ if bm.operations.step then
/* step */
void $(bm.name)_step(ubx_block_t *b)
{
	/* struct $(bm.name)_info *inf = (struct $(bm.name)_info*) b->private_data; */
        ubx_info(b, "%s", __func__);
}
@ end

]], { table=table, bm=bm } )

   if not res then error(str) end
   fd:write(str)
end

--- Generate a simple blockdiagram interface of an ubx block.
-- @param bm block model
-- @param fd open file to write to (optional, default: io.stdout)
local function generate_bd_system(fd, bm, outdir)
   fd = fd or io.stdout
   local res, str = utils.preproc(
[[
-- a minimal blockdiagram to start the block

return bd.system
{
   imports = {
      "stdtypes",
      "$(bm.name)",
   },

   blocks = {
      { name="$(bm.name)_1", type="$(bm.name)" },
   },
}

]], { table=table, bm=bm, outdir=outdir } )

   if not res then error(str) end
   fd:write(str)
end


--- Generate code according to the given code generation table.
-- For each entry in code_gen_table, open file and call fun passing
-- block_model as first and the file handle as second arg
-- @param cgt code generate table
-- @param outdir directory prefix
-- @param force_overwrite ignore overwrite flag on individual entries
function generate(cgt, outdir, force_overwrite)

   local function file_open(fn, overwrite)
      if not overwrite and utils.file_exists(fn) then return false end
      return assert(io.open(fn, "w"))
   end

   utils.foreach(function(e)
		    local file = outdir.."/"..e.file
		    local fd = file_open(file, e.overwrite or force_overwrite)
		    if fd then
		       print("    generating ".. file)
		       e.fun(fd, unpack(e.funargs))
		       fd:close()
		    else
		       print("    skipping existing "..file)
		    end
		    if e.executable then os.execute("chmod +x "..file) end
		 end, cgt)
end


--
-- Program enters here
--

local opttab=utils.proc_args(arg)

local block_model_file
local force_overwrite
local c_ext = '.c'
local h_ext = '.h'

if #arg==1 or opttab['-h'] then usage(); os.exit(1) end

-- load and check config file
if not (opttab['-c'] and opttab['-c'][1]) then
   print("missing config file option (-c)"); os.exit(1)
end

block_model_file=opttab['-c'][1]

local suc, block_model = pcall(dofile, block_model_file)

if not suc then
   print(block_model)
   print("ubx-genblock failed to load block config file "..ts(block_model_file))
   os.exit(1)
end

if block_model:validate(false) > 0 then
   block_model:validate(true)
   os.exit(1)
end

-- only check, don't generate
if opttab['-check'] then
   print("Ok!")
   os.exit(1)
end

if opttab['-force'] then
   force_overwrite=true
end

if opttab['-cpp'] then
   block_model.cpp = true
end

-- handle output directory
if not (opttab['-d'] and opttab['-d'][1]) then
   print("missing output directory (-d)"); os.exit(1)
end

local outdir=opttab['-d'][1]

if not utils.file_exists(outdir) then
   if os.execute("mkdir -p "..outdir) ~= 0 then
      print("creating dir "..outdir.." failed")
      os.exit(1)
   end
end

if not utils.file_exists(outdir.."/types") then
   if os.execute("mkdir -p "..outdir.."/types") ~= 0 then
      print("creating dir "..outdir.."/types ".." failed")
      os.exit(1)
   end
end

if block_model.cpp then c_ext = '.cpp' end
if block_model.cpp then h_ext = '.hpp' end

-- static part
local codegen_tab = {
   { fun = generate_bootstrap, funargs = { block_model }, file = "bootstrap", overwrite = true, executable = true },
   { fun = generate_configure_ac, funargs = { block_model }, file = "configure.ac", overwrite = false },
   { fun = generate_automakefile, funargs = { block_model }, file = "Makefile.am", overwrite = false },
   { fun = generate_block_if, funargs = { block_model } , file = block_model.name..h_ext, overwrite = true },
   { fun = generate_block_body, funargs = { block_model }, file = block_model.name..c_ext, overwrite = false },
   { fun = generate_bd_system, funargs = { block_model, outdir }, file = block_model.name..".usc", overwrite = false },
}

-- add types
for i,t in ipairs(block_model.types or {}) do
   if t.class=='struct' then
      codegen_tab[#codegen_tab+1] =
	 { fun=generate_struct_type, funargs={ t }, file="types/"..t.name..".h", overwrite=false }
   elseif t.type=='enum' then
      print("   ERROR: enum not yet supported")
   end
end

generate(codegen_tab, outdir, force_overwrite)
